# svg_to_csv.py

from __future__ import division

import re
import itertools

from xml.etree.ElementTree import ElementTree

from ot_simulator.template.template_funs import frange

def svg_to_csv(svg_file_in, csv_file_out, x_start, x_end, x_spacing = 0.01):
    to_csv(csv_file_out, gen(svg_file_in, x_start, x_end, x_spacing))

def to_csv(filename, points):
    with open(filename, 'wt') as f:
        for x, y in points:
            print >> f, "{0:.4f},{1:.4f}".format(x, y)

def gen(filename, x_start, x_end, x_spacing = 0.01):
    r'''Returns an unsorted list of (x, y) pairs.
    '''
    tree = ElementTree()
    tree.parse(filename)
    ranges = sorted(parse_children(tree.getroot()),
                    key=lambda x: x.get_x_range())
    min_x = ranges[0].get_x_range()[0]
    max_x = ranges[-1].get_x_range()[1]
    scale = (x_end - x_start) / (max_x - min_x) 
    x_offset = x_start - scale * min_x
    if x_end < x_start and x_spacing > 0.0:
        x_spacing = -x_spacing
    assert abs(scale * min_x + x_offset - x_start) < 1e-9
    assert abs(scale * max_x + x_offset - x_end) < 1e-9
    return itertools.chain.from_iterable(
             r.enumerate(scale, x_offset, 0.0, x_spacing)
             for r in ranges)

def parse_children(element):
    ans = []
    for c in element.getchildren():
        tag = strip_name(c.tag)
        if tag == 'g': ans.extend(parse_g(c))
        elif tag == 'path': ans.extend(parse_path(c))
    return ans

def parse_g(g):
    if parse_style(g.get('style', '')).get('stroke', 'True') != 'none':
        return parse_children(g)
    return []

def parse_path(path):
    if parse_style(path.get('style', '')).get('stroke', 'True') != 'none':
        return parse_d(path.get('d', ''))
    return []

def parse_d(d):
    ans = []
    prev_location = None
    for command in parse_d_str(d):
        c = command[0]
        g = globals()
        if c in g:
            fn = g[c]
        else:
            raise NotImplemented("svg_to_csv: path command {0!r}".format(c))
        ranges, prev_location = fn(command[1:], prev_location)
        ans.extend(ranges)
    return ans

def M(d, prev_location):
    final_location = d[:2]
    if len(d) > 2: return L(d[2:], final_location)
    return [], final_location

def m(d, prev_location):
    if prev_location: final_location = point_add(prev_location, d[:2])
    else: final_location = d[:2]
    if len(d) > 2: return l(d[2:], final_location)
    return [], final_location

def L(d, prev_location):
    final_location = d[:2]
    ranges = [line(prev_location, final_location)]
    while len(d) > 2:
        d = d[2:]
        prev_location = final_location
        final_location = d[:2]
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def l(d, prev_location):
    final_location = point_add(prev_location, d[:2])
    ranges = [line(prev_location, final_location)]
    while len(d) > 2:
        d = d[2:]
        prev_location = final_location
        final_location = point_add(prev_location, d[:2])
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def H(d, prev_location):
    final_location = d[0], prev_location[1]
    ranges = [line(prev_location, final_location)]
    while len(d) > 1:
        d = d[1:]
        prev_location = final_location
        final_location = d[0], prev_location[1]
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def h(d, prev_location):
    final_location = prev_location[0] + d[0], prev_location[1]
    ranges = [line(prev_location, final_location)]
    while len(d) > 1:
        d = d[1:]
        prev_location = final_location
        final_location = prev_location[0] + d[0], prev_location[1]
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def V(d, prev_location):
    final_location = prev_location[0], d[0]
    ranges = [line(prev_location, final_location)]
    while len(d) > 1:
        d = d[1:]
        prev_location = final_location
        final_location = prev_location[0], d[0]
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def v(d, prev_location):
    final_location = prev_location[0], prev_location[1] + d[0]
    ranges = [line(prev_location, final_location)]
    while len(d) > 1:
        d = d[1:]
        prev_location = final_location
        final_location = prev_location[0], prev_location[1] + d[0]
        ranges.append(line(prev_location, final_location))
    return ranges, final_location

def C(d, prev_location):
    x1, y1, x2, y2, x, y = d[:6]
    final_location = x, y
    ranges = [cubic_bezier(prev_location, final_location, (x1, y1), (x2, y2))]
    while len(d) > 6:
        d = d[6:]
        prev_location = final_location
        x1, y1, x2, y2, x, y = d[:6]
        final_location = x, y
        ranges.append(cubic_bezier(prev_location, final_location,
                                   (x1, y1), (x2, y2)))
    return ranges, final_location

def c(d, prev_location):
    x1, y1, x2, y2, x, y = d[:6]
    final_location = point_add(prev_location, (x, y))
    ranges = [cubic_bezier(prev_location, final_location,
                           point_add(prev_location, (x1, y1)),
                           point_add(prev_location, (x2, y2)))]
    while len(d) > 6:
        d = d[6:]
        prev_location = final_location
        x1, y1, x2, y2, x, y = d[:6]
        final_location = point_add(prev_location, (x, y))
        ranges.append(cubic_bezier(prev_location, final_location,
                                   point_add(prev_location, (x1, y1)),
                                   point_add(prev_location, (x2, y2))))
    return ranges, final_location

def strip_name(name):
    r'''Strips the ``{url}`` prefix (if any) from ``name``.

        >>> strip_name('{bogus}svg')
        'svg'
        >>> strip_name('plain')
        'plain'
    '''
    return name[name.find('}')+1:]

def parse_style(style):
    r'''Parses svg style declaration and returns a dict.

        >>> sorted(parse_style('stroke :none; fill : none').items())
        [('fill', 'none'), ('stroke', 'none')]
        >>> parse_style('')
        {}
    '''
    if not style: return {}
    return dict((x.strip() for x in setting.split(':'))
                for setting in style.split(';'))

Letter_re = re.compile(r'([a-zA-Z])')
Spaces_re = re.compile(r'(  +|[\t\r\n,])')

def parse_d_str(d):
    r'''Yields tuples of (letter, float...) from ``d``.

        >>> tuple(parse_d_str('M 1,2 L3,4,5,6'))
        (('M', 1.0, 2.0), ('L', 3.0, 4.0, 5.0, 6.0))
    '''
    d = Spaces_re.sub(' ', Letter_re.sub(r' \1 ', d)).strip().split()
    #print "got", d
    t = []
    for x in d:
        if x.isalpha():
            if t: yield tuple(t)
            t = [x]
        else:
            t.append(float(x))
    if t: yield tuple(t)

def point_add(*points):
    return sum(p[0] for p in points), sum(p[1] for p in points)

def point_mul(c, p):
    x, y = p
    return c * x, c * y

class line(object):
    r'''Line segment from ``start`` to ``end``.
    '''
    def __init__(self, start, end):
        # enumerate in increasing x values
        if start[0] > end[0]:
            start, end = end, start

        start_x, start_y = start
        end_x, end_y = end

        self.x_range = start_x, end_x

        # vertical lines are not functions!
        if start_x == end_x:
            raise ValueError("vertical lines not allowed in svg functions")

        # we use y = m*x + b
        self.m = (end_y - start_y) / (end_x - start_x)
        self.b = start_y - self.m * start_x
        assert abs(self.m*start_x + self.b - start_y) < 1e-9
        assert abs(self.m*end_x + self.b - end_y) < 1e-9

    def get_x_range(self):
        return self.x_range

    def enumerate(self, scale, x_offset, y_offset, x_spacing):
        # y = m*x + b0
        # scale * y + y_offset = m*(scale*x + x_offset) + b1
        # scale * (m*x + b0) + y_offset = m*(scale*x + x_offset) + b1
        # scale*m*x + scale*b0 + y_offset = scale*m*x + m*x_offset + b1
        # scale*m*x + scale*b0 + y_offset - scale*m*x - m*x_offset = b1
        # scale*b0 + y_offset - m*x_offset = b1
        b1 = scale * self.b + y_offset - self.m * x_offset
        return ((x, self.m*x + b1)
                for x in frange(x_spacing,
                                scale * self.x_range[0] + x_offset,
                                scale * self.x_range[1] + x_offset))

class cubic_bezier(object):
    r'''Cubic bezier curve from ``start`` to ``end``.
    '''
    def __init__(self, start, end, start_control_pt, end_control_pt):
        r'''The cubic bezier function.

        P0 = ``start``
        P1 = ``start_control_pt``
        P2 = ``end_control_pt``
        P3 = ``end``
        t in [0,1]

        B(t) = (1 - t)^3\*P0 + 3(1 - t)^2t\*P1 + 3(1 - t)t^2\*P2 + t^3\*P3
        '''
        # enumerate in increasing x values
        if start[0] <= end[0]:
            self.x_range = start[0], end[0]
            self.y_range = start[1], end[1]
            self.P0, self.P3 = start, end
            self.P1, self.P2 = start_control_pt, end_control_pt
        else:
            self.x_range = end[0], start[0]
            self.y_range = end[1], start[1]
            self.P0, self.P3 = end, start
            self.P1, self.P2 = end_control_pt, start_control_pt

    def get_x_range(self):
        return self.x_range

    def B(self, t):
        return point_add(point_mul((1.0 - t)**3, self.P0),
                         point_mul(3.0*(1.0 - t)**2 * t, self.P1),
                         point_mul(3.0*(1.0 - t) * t**2, self.P2),
                         point_mul(t**3, self.P3))

    def enumerate(self, scale, x_offset, y_offset, x_spacing):
        # figure out t_spacing
        range = 1.5 * scale * max(self.x_range[1] - self.x_range[0],
                                  abs(self.y_range[1] - self.y_range[0]))
        num_to_gen = range / x_spacing
        t_spacing = 1.0 / num_to_gen

        return (point_add((x_offset, y_offset), point_mul(scale, self.B(t)))
                for t in frange(t_spacing, 0.0, 1.0))

